PyTorch实战: 使用卷积神经网络对照片进行分类
本文任务
我们接下来需要用CIFAR-10数据集进行分类,步骤如下:
使用torchvision 加载并预处理CIFAR-10数据集
定义网络
定义损失函数和优化器
训练网络并更新网络参数
测试网络
对卷积不了解的同学建议先阅读
注意:文章末尾含有项目jupyter notebook实战教程下载可供大家课后实战操作
一、CIFAR-10数据加载及预处理
CIFAR-10
是一个常用的彩色图片数据集,它有 10 个类别,分别是 airplane、automobile、bird、cat、deer、dog、frog、horse、ship和 truck
。每张图片都是 3*32*32
,也就是 三通道彩色图片,分辨率 32*32
。
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
import torch as t
#可以把Tensor转化为Image,方便可视化
show = ToPILImage()
#先伪造一个图片的Tensor,用ToPILImage显示
fake_img = t.randn(3, 32, 32)
#显示图片
show(fake_img)
第一次运行torchvision会自动下载CIFAR-10数据集,大约163M。这里我将数据直接放到项目 data文件夹
中。
cifar_dataset = tv.datasets.CIFAR10(root='data',
train=True,
download=True
)
imgdata, label = cifar_dataset[90]
print('label: ', label)
print('imgdata的类型:',type(imgdata))
imgdata
运行结果
Files already downloaded and verified
label: 2
imgdata的类型: <class 'PIL.Image.Image'>
注意,数据集中的照片数据是以 PIL.Image.Image类
形式存储的,在我们加载数据时,要注意将其转化为 Tensor类
。
def dataloader(train):
transformer = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=(0.5, 0.5, 0.5),
std = (0.5, 0.5, 0.5))
])
cifar_dataset = tv.datasets.CIFAR10(root='data', #下载的数据集所在的位置
train=train, #是否为训练集。
download=True, #设置为True,不用再重新下载数据
transform=transformer
)
loader = t.utils.data.DataLoader(
cifar_dataset,
batch_size=4,
shuffle=True, #打乱顺序
num_workers=2 #worker数为2
)
return loader
classes=('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
#训练集和测试集的加载器
trainloader = dataloader(train=True)
testloader = dataloader(train=False)
运行结果
Files already downloaded and verified
Files already downloaded and verified
DataLoader是一个可迭代的对象,它将dataset返回的每一条数据样本拼接成一个batch,并提供多线程加速优化和数据打乱等操作。当程序对 cirfar_dataset
的所有数据遍历完一遍, 对Dataloader也完成了一次迭代。
dataiter = iter(trainloader)
#返回四张照片及其label
images, labels = dataiter.next()
#打印多张照片
show(tv.utils.make_grid(images))
#显示images中的第三张照片
show(images[2])
二、定义网络
最早的卷积神经网络LeNet为例,学习卷积神经网络。
2.1 第一个convolutions层
图中显示是单通道照片,但是由于我们的数据集中的照片是三通道照片。所以
该层输入的是 三通道图片
,图片长宽均为32,那么通过kernel_size=5的卷积核卷积后的尺寸为(32-5+1)=28
同时要注意,第一个convolution中,图片由 三通道变为6通道
, 所以在此卷积过程中,in_channels=3, out_channels=6
nn.Conv2d(in_channels=3,
out_channels=6,
kernel_size=5)
2.2 第一subsampling层
该层输入数据是6通道,输出还为6通道,但是图片的长宽从28变为14,我们可以使用池化层来实现尺寸缩小一倍。这里我们使用MaxPool2d(2, 2)
nn.MaxPool2d(kernel_size=2,
stride=2)
2.3 第二个convolutions层
该层输入的是6通道数据,输出为16通道数据,且图片长宽从14变为10。这里我们使用
nn.Conv2d(in_channels=6,
out_channels=16,
kernel_size=5)
2.4 全连接层作用
在此之前的卷积层和池化层都属于特征工程层,用于从数据中抽取特征。而之后的多个全连接层,功能类似于机器学习中的模型,用于学习特征数据中的规律,并输出预测结果。
2.5 第一全连接层full connection
第二个convolutions层输出的 数据形状为 (16, 5, 5) 的数组
,是一个三维数据。
而在全连接层中,我们需要将其 展平为一个一维数据(样子类似于列表,长度为16\*5\*5)
nn.Linear(in_features=16*5*5,
out_features=120) #根据图中,该输出为120
2.6 第二全连接层
该层的输入是一维数组,长度为120,输出为一维数组,长度为84.
nn.Linear(in_features=120,
out_features=84) #根据图中,该输出为84
2.7 第三全连接层
该层的输入是一维数组,长度为84,输出为一维数组,长度为10,该层网络定义如下
nn.Linear(in_features=84,
out_features=10) #根据图中,该输出为10
注意:
这里的长度10的列表,可以看做输出的label序列。例如理想情况下
output = [1, 0, 0, 0, 0, 0, 0 ,0, 0 ,0]
该output表示 input数据
经过该神经网络运算得到的 预测结果
显示的类别是 第一类
同理,理想情况下
output2 = [0, 1, 0, 0, 0, 0, 0 ,0, 0 ,0]
该output2表示 input数据
经过该神经网络运算得到的 预测结果
显示的类别是 第二类
根据前面对LeNet网络的解读,现在我们用pytorch来定义LeNet网络结构
import torch
import torch.nn as nn
class LeNet(nn.Module):
def __init__(self):
#Net继承nn.Module类,这里初始化调用Module中的一些方法和属性
nn.Module.__init__(self)
#定义特征工程网络层,用于从输入数据中进行抽象提取特征
self.feature_engineering = nn.Sequential(
nn.Conv2d(in_channels=3,
out_channels=6,
kernel_size=5),
#kernel_size=2, stride=2,正好可以将图片长宽尺寸缩小为原来的一半
nn.MaxPool2d(kernel_size=2,
stride=2),
nn.Conv2d(in_channels=6,
out_channels=16,
kernel_size=5),
nn.MaxPool2d(kernel_size=2,
stride=2)
)
#分类器层,将self.feature_engineering中的输出的数据进行拟合
self.classifier = nn.Sequential(
nn.Linear(in_features=16*5*5,
out_features=120),
nn.Linear(in_features=120,
out_features=84),
nn.Linear(in_features=84,
out_features=10),
)
def forward(self, x):
#在Net中改写nn.Module中的forward方法。
#这里定义的forward不是调用,我们可以理解成数据流的方向,给net输入数据inpput会按照forward提示的流程进行处理和操作并输出数据
x = self.feature_engineering(x)
x = x.view(-1, 16*5*5)
x = self.classifier(x)
return x
实例化神经网络LeNet
net = LeNet()
net
运行结果
LeNet(
(feature_engineering): Sequential(
(0): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(classifier): Sequential(
(0): Linear(in_features=400, out_features=120, bias=True)
(1): Linear(in_features=120, out_features=84, bias=True)
(2): Linear(in_features=84, out_features=10, bias=True)
)
)
我们随机传入一批照片(batch_size=4) ,将其输入给net,看输出的结果是什么情况。
注意:
pytorch中输入的数据必须是batch数据(批数据)
dataiter = iter(trainloader)
#返回四张照片及其label
images, labels = dataiter.next()
outputs = net(images)
outputs
运行结果
tensor([[ 0.1963, 0.0203, 0.0887, -0.0789, -0.0027, -0.0429, -0.1119, 0.0080,
0.0007, -0.0901],
[ 0.2260, 0.0246, 0.0498, -0.0188, 0.0207, -0.0541, -0.0943, 0.0431,
-0.0204, -0.1023],
[ 0.2168, 0.0280, 0.0463, -0.0055, -0.0017, -0.0504, -0.0897, 0.0385,
-0.0229, -0.1030],
[ 0.2025, 0.0579, 0.0527, -0.0038, -0.0300, -0.0474, -0.0952, 0.0698,
-0.0145, -0.0620]], grad_fn=<ThAddmmBackward>)
t.max(input, dim)
input:传入的tensor
dim: tensor的方向。dim=1表示按照行方向计算最大值
t.max(outputs, dim=1)
运行结果
(tensor([0.1963, 0.2260, 0.2168, 0.2025], grad_fn=<MaxBackward0>),
tensor([0, 0, 0, 0]))
上述的操作,找到了outputs中四个最大的值,及其对应的index(该index可以理解为label)
三、定义损失函数和优化器
神经网络强大之处就在于 反向传播
,通过比较 预测结果
与 真实结果
, 修整网络参数
。
这里的 比较
就是 损失函数
,而 修整网络参数
就是 优化器
。
这样充分利用了每个训练数据,使得网络的拟合和预测能力大大提高。
from torch import optim
#定义交叉熵损失函数
criterion = nn.CrossEntropyLoss()
#随机梯度下降SGD优化器
optimizer = optim.SGD(params = net.parameters(),
lr = 0.001)
四、训练网络
所有网络的训练的流程都是类似的,不断执行(轮):
给网络输入数据
前向传播+反向传播
更新网络参数
遍历完一遍数据集称为一个epoch,这里我们进行 2个epoch
轮次的训练。
epochs = 10
average_loss_series = []
for epoch in range(epochs):
running_loss = 0.0
for i, data in enumerate(trainloader):
inputs, labels = data
#inputs, labels = Variable(inputs), Variable(labels)
#梯度清零
optimizer.zero_grad()
#forward+backward
outputs = net(inputs)
#对比预测结果和labels,计算loss
loss = criterion(outputs, labels)
#反向传播
loss.backward()
#更新参数
optimizer.step()
#打印log
running_loss += loss.item()
if i % 2000 == 1999: #每2000个batch打印一次训练状态
average_loss = running_loss/2000
print("[{0},{1}] loss: {2}".format(epoch+1, i+1, average_loss))
average_loss_series.append(average_loss)
running_loss = 0.0
运行结果
[1,2000] loss: 2.284719424366951
[1,4000] loss: 2.1300598658323286
[1,6000] loss: 2.0143098856806754
[1,8000] loss: 1.9478365245759488
[1,10000] loss: 1.9135449583530426
[1,12000] loss: 1.8653237966001033
[2,2000] loss: 1.8014366626143457
[2,4000] loss: 1.737443323969841
[2,6000] loss: 1.6933535016775132
[2,8000] loss: 1.6476907352507115
[2,10000] loss: 1.6234023304879666
[2,12000] loss: 1.5863604183495044
[3,2000] loss: 1.5544855180978776
[3,4000] loss: 1.539060534775257
[3,6000] loss: 1.5500386973917484
[3,8000] loss: 1.5407403408288955
[3,10000] loss: 1.493699783280492
[3,12000] loss: 1.4957395897060632
[4,2000] loss: 1.4730096785128117
[4,4000] loss: 1.4749664356559515
[4,6000] loss: 1.4479290856420994
[4,8000] loss: 1.445657522082329
[4,10000] loss: 1.4586472637057304
[4,12000] loss: 1.4320134285390378
[5,2000] loss: 1.406113230422139
[5,4000] loss: 1.4196837954670192
[5,6000] loss: 1.3951636335104705
[5,8000] loss: 1.3933502195328473
[5,10000] loss: 1.3908299638181925
[5,12000] loss: 1.3908768535405398
[6,2000] loss: 1.3397984126955271
[6,4000] loss: 1.3737898395806551
[6,6000] loss: 1.360704499706626
[6,8000] loss: 1.3652801268100738
[6,10000] loss: 1.334371616870165
[6,12000] loss: 1.312294240474701
[7,2000] loss: 1.3097571679353714
[7,4000] loss: 1.3236577164530754
[7,6000] loss: 1.310647354334593
[7,8000] loss: 1.3016219032108785
[7,10000] loss: 1.2931814943552018
[7,12000] loss: 1.2910259604007006
[8,2000] loss: 1.2796987656354903
[8,4000] loss: 1.2650054657310248
[8,6000] loss: 1.2713083022236824
[8,8000] loss: 1.258927255064249
[8,10000] loss: 1.275728213787079
[8,12000] loss: 1.2612977192252874
[9,2000] loss: 1.2273035216629504
[9,4000] loss: 1.25000972096622
[9,6000] loss: 1.2236297953873874
[9,8000] loss: 1.2251979489773512
[9,10000] loss: 1.2623697004914283
[9,12000] loss: 1.2501848887503146
[10,2000] loss: 1.2257770787626505
[10,4000] loss: 1.2277075409144163
[10,6000] loss: 1.2050671626776457
[10,8000] loss: 1.2159633481949568
[10,10000] loss: 1.210464821562171
[10,12000] loss: 1.2225491935014725
五、测试网络
5.1 打印误差曲线
%matplotlib inline
import matplotlib.pyplot as plt
x = range(0, 60)
plt.figure()
plt.plot(x, average_loss_series)
5.2 查看训练的准确率
我们使用测试集检验训练的神经网络的性能。
def correct_rate(net, testloader):
correct = 0
total = 0
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = t.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted==labels).sum()
return 100*correct/total
correct = correct_rate(net, testloader)
print('10000张测试集中准确率为: {}%'.format(correct))
运行结果
10000张测试集中准确率为: 57%
数据集一共有10种照片,且每种照片数量相等。所以理论上,我们猜测对每一张照片的概率为10%。
而通过我们神经网络LeNet预测的准确率达到 57%
,证明网络确实学习到了规律。